很久很久以前,我還在寫Android的時候,要做異步處理有很多選擇。從早期的AsyncTask,中期的Rx,到後來的Coroutine,不只是異步處理的語法越來越直覺,多執行緒的切換也更加容易。於是開始學習Flutter之後,遇到了Dart async/await/Future,不知為何就擅自認定了,它們同樣是可以簡單地把工作丟到其它執行緒處理的語法,直到經過了一段長到我不願意承認的時間後,我才發現事情並沒有那麼單純。
如果你也有跟我一樣的幻覺,現在是時候醒過來了。異步處理不等於平行處理。當我們呼叫async函數,使用Future時,其實只是在告訴Dart「嘿,這段程式碼不急,你有空再幫我處理就好了。」於是Dart會把這份工作加進某個人的待辦清單裡面,而實際上你指派的所有的工作都是同一個人在處理。在Dart裡面,這個人叫做Isolate,而那份待辦清單叫做Event Loop。
基本上,Isolate就是Dart程式的Process,多個Isolate之間不會共享記憶體,而是靠互相發送訊息來溝通。和一般Process不同的是,每一個Isolate只會有單一執行緒,而我們在這個執行緒上使用所有的asyn/await/Future,最後都是同樣在這個執行緒上,根據Event Loop機制執行的。
當我們的Dart程式從main()開始執行時,就會啟動一個叫做main的Isolate。如果你還有印象,我們其實在之前的文章也曾經看過它:
一般來說,我們寫的絕大多數Flutter程式碼都是在這個main Isolate裡執行的。如果我們有一些比較繁重的工作,想要實現真正的平行處理的話,我們就必須啟動一個新的Isolate。
之前我們提到Isolate之間是靠message來溝通的,然而實現Isolate雙向溝通的Dart API有點不是那麼容易理解,所以讓我們一步一步來吧。
首先來看看Dart提供的產生新Isolate的函數:
external static Future<Isolate> spawn<T>(
void entryPoint(T message), T message,
{bool paused = false,
bool errorsAreFatal = true,
SendPort? onExit,
SendPort? onError,
@Since("2.3") String? debugName});
這裡必填的只有兩個參數,entryPoint
就相當於是new Isolate的main,而message
就是main會收到的參數。
首先讓我們試著建立一個Isolate,並傳遞一則訊息:
void main() async {
final newIsolate = await Isolate.spawn(newIsolateMain, "Hello I'm main Isolate!");
}
void newIsolateMain(String message) {
print("new Isolate received: $message");
}
我們呼叫Isolate.spawn,傳入newIsolateMain
和message,新的Isolate就會從newIsolateMaind開始執行,並收到message作為參數。這裡唯一要注意的是,newIsolateMain
必須是global或static function。
接下來如果我想從new Isolate得到回覆呢?事情馬上就開始有趣起來了:
void main() async {
final mainReceivePort = ReceivePort();
final newIsolate = await Isolate.spawn(newIsolateMain, mainReceivePort.sendPort);
final message = await mainReceivePort.first;
print("main Isolate received: $message");
}
void newIsolateMain(SendPort mainSendPort) {
mainSendPort.send("Hello from new Isolate");
}
Isolate的message是在port之間傳遞的,有點類似socket的概念。我們可以在Isolate中建立任意數量的ReceivePort
,用來接收來自其它Isolate的訊息。每一個ReceivePort都會自帶一個SendPort
,可以傳遞給其它Isolate,讓對方發送訊息回來。怎麼把我的SendPort傳遞給對方呢?當然是靠對方給我的SendPort來傳遞我的SendPort...
的確,一開始main Isolate根本沒有new Isolate的SendPort,我們唯一的機會就是把main的SendPort在Isolate.spawn時傳遞給對方。錯過了這個機會,未來就再也無法建立任何通訊了。
把SendPort傳入newIsolateMain
之後,new Isolate就可以傳遞訊息回來。這時候在main裡面就可以從mainReceivePort
取回message。這裡的mainReceivePort實作了Stream
,所以我們可以用first
, listen
等各種方式來處理取回的message。
不過,如果我們最初傳遞參數給new Isolate的機會被SendPort
用掉了,那我們該如何傳遞真正執行函數所需的參數呢?例如我們想讓new Isolate幫我們計算n+1
,該如何把這個n
傳過去?這下事情就變得非常有趣了:
import 'dart:async';
import 'dart:isolate';
void main() async {
// 1. 建立ReceivePort
final mainPort = ReceivePort();
// 2. 傳遞sendPort給new Isolate
final newIsolate = await Isolate.spawn(newIsolateMain, mainPort.sendPort);
mainPort.listen((message) {
// 5. main Isolate接收到new Isolate的sendPort
if (message is SendPort) {
int n = 42;
// 6. 傳遞slowPlusOne所需的參數給new Isolate
message.send(n);
print("main Isolate: Message sent to new Isolate: $n");
// 10. 收到來自new Isolate的計算結果
} else {
print("main Isolate: Received message from new Isolate: $message");
}
});
}
void newIsolateMain(SendPort mainSendPort) {
// 3. new Isolate同樣建立ReceivePort
final newPort = ReceivePort();
// 4. 將sendPort回傳給main Isolate
mainSendPort.send(newPort.sendPort);
newPort.listen((message) async {
print("new Isolate: Received message from main Isolate: $message");
// 7. new Isolate接收到slowPlusOne的參數
if (message is int) {
// 8. 執行slowPlusOne
final value = await slowPlusOne(message);
// 9. 將結果回傳給main Isolate
mainSendPort.send(value);
print("new Isolate: Message sent to main Isolate: $value");
}
});
}
Future<int> slowPlusOne(int n) => Future.delayed(Duration(seconds: 5), () => n+1);
總之,為了達成雙向溝通,我們必須先把main的sendPort傳給new,再把new的sendPort傳回來,就像是在進行hand-shake一樣。完成之後才能開始正常的傳遞參數,呼叫函數計算後回傳結果。不過,這時候我們是直接在new Isolate裡面固定呼叫slowPlusOne函數,如果我們也想把函數當作參數傳遞給new Isolate呢?
讓我們來建立一個可以呼叫任何函數的runInBackground(function, argument)
吧。到這裡其實已經沒有關於Isolate的新鮮事了,只是稍微再加入一點functional的觀念而已,想挑戰的人也可以自己試著實作看看:
import 'dart:async';
import 'dart:isolate';
void main() async {
final result = await runInBackground(slowPlusOne, 42);
print(result);
}
typedef OneArgumentFunction<T, R> = Future<R> Function(T message);
Future<R> runInBackground<T, R>(OneArgumentFunction<T, R> function, T argument) async {
final mainPort = ReceivePort();
final newIsolate = await Isolate.spawn(newIsolateMain, mainPort.sendPort);
// 0. 這讓我們可以重複 await broadcast.first
final broadcast = mainPort.asBroadcastStream();
// 1. 接收來自new Isolate的SendPort
final newSendPort = await broadcast.first as SendPort;
// 2. 將function和argument一起傳遞給new Isolate
newSendPort.send([function, argument]);
// 6. 接收來自new Isolate的function執行結果
final result = await broadcast.first as R;
return result;
}
void newIsolateMain(SendPort mainSendPort) {
final newPort = ReceivePort();
mainSendPort.send(newPort.sendPort);
newPort.listen((message) async {
// 3. 接收來自main Isolate的function/argument
final function = message[0];
final argument = message[1];
// 4. 執行並等待結果
final result = await function(argument);
// 5. 將結果回傳
mainSendPort.send(result);
});
}
Future<int> slowPlusOne(int n) => Future.delayed(Duration(seconds: 5), () => n+1);
這裡唯一須要注意的就是mainPort.asBroadcastStream()
。因為我們會從mainPort收到兩個訊息,new Isolate的SendPort和函數執行結果。如果我們使用mainPort.listen
(callback style),就沒辦法把結果從runInBackground再傳回main,我們必須使用await mainPort.first
(sequential style)來取得結果才能再回傳。但因為呼叫first取得結果後stream就會被關閉,因此我們必須將它轉成broadcastStream來避免這個問題。
完成了!恭喜你...重新發明了Dart提供的compute
!其實我們也可以這樣:
import 'package:flutter/foundation.dart';
void main() async {
final result = await compute(slowPlusOne, 42);
print(result);
}
當然compute的實作比我們的還要更複雜一點,但概念大致上都是一樣的。雖然大多數簡單的背景執行需求都可以靠compute來達成,但理解了它背後的這些Isolate相關細節之後,未來你也可以依照自己的需來實作所需的compute函數,實現更複雜的平行處理。
Hello,請問一下,compute 和 isolate 最大的差別是什麼呀...,想問一下正常大型專案再處理背景處理時,會使用 compute 還是 isolate 比較多啊?
Isolate是Dart語言本身的多執行緒/異步處理機制,直接使用它的API比較複雜。compute是建立在Isolate之上的Dart內建函數,使用上比isolate的spawn/send message簡單很多。一般沒有特殊需求的話用compute就可以了,這篇主要是詳細說明compute和其背後的isolate如何運作。
okok,了解,你的文章真的寫得很好,使我的 flutter 更上一層樓。謝謝~